package com.wxhntmy.redis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.JedisPooled;
import redis.clients.jedis.params.SetParams;

import java.net.SocketTimeoutException;
import java.util.concurrent.TimeUnit;

/**
 * Redis 分布式锁
 *
 * @author chenwc
 * @date 2023/5/21 17:01
 */
public class JedisLock {

    private final static Logger log = LoggerFactory.getLogger(JedisLock.class);
    //抢锁成功返回状态
    private final static String RESULT_OK = "OK";
    //解锁成功返回状态
    private static final Long UNLOCK_SUCCESS = 1L;
    private JedisPooled client;
    private LockState lockState;
    private KeepAliveTask keepAliveTask;
    //抢锁的时间间隔
    private int sleepMillisecond;

    /**
     * 构造函数
     *
     * @param client     JedisPooled客户端
     * @param key        键
     * @param value      值
     * @param ttlSeconds 锁的过期时间
     */
    public JedisLock(JedisPooled client, String key, String value, int ttlSeconds) {
        //1.准备客户端
        this.client = client;
        this.lockState = new LockState(key, value, ttlSeconds);
        //抢锁的重试间隔
        this.sleepMillisecond = (ttlSeconds * 1000) / 3;
    }

    /**
     * 加锁
     *
     * @param waitTime 等待时间
     * @param waitUnit 时间单位
     * @return 加锁是否成功，成功返回true，失败返回false
     * @throws DtLockException 锁异常
     */
    public boolean tryLock(long waitTime, TimeUnit waitUnit) throws DtLockException {
        long totalMillisSeconds = waitUnit.toMillis(waitTime);
        long start = System.currentTimeMillis();
        //重试，直到成功或超过指定时间
        while (true) {
            // 抢锁
            try {
                //使用 SET 的扩展指令加锁（SET key value [EX seconds][px milliseconds] [NX|XX]）
                SetParams params = SetParams.setParams().nx().ex(lockState.getLeaseTTL());
                String result = client.set(lockState.getLockKey(), lockState.getLockValue(), params);
                if (RESULT_OK.equals(result)) {
                    manualKeepAlive();
                    log.info("[jedis-lock] lock success 线程：{} 加锁成功，key:{} , value：{}", Thread.currentThread().getName(), lockState.getLockKey(), lockState.getLockValue());
                    //抢锁成功
                    lockState.setLockSuccess(true);
                    return true;
                } else {
                    //抢锁时间大于等待时间，返回失败
                    if (System.currentTimeMillis() - start >= totalMillisSeconds) {
                        log.info("[jedis-lock] lock fail 线程：{} 等待加锁超时，key:{} , value：{}", Thread.currentThread().getName(), lockState.getLockKey(), lockState.getLockValue());
                        return false;
                    }
                    //抢锁的重试时间间隔
                    Thread.sleep(sleepMillisecond);
                }
            } catch (Exception e) {
                Throwable cause = e.getCause();
                if (cause instanceof SocketTimeoutException) {//忽略网络抖动等异常
                }
                log.error("[jedis-lock] lock failed:" + e);
                throw new DtLockException("[jedis-lock] lock failed:" + e.getMessage(), e);
            }

        }
    }

    /**
     * 此实现中忽略，网络通信异常部分的处理，可参考tryLock
     *
     * @throws DtLockException 锁异常
     */
    public void unlock() throws DtLockException {
        try {
            // 首先停止续约
            if (keepAliveTask != null) {
                keepAliveTask.close();
            }
            //使用 lua 解锁
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = client.eval(script, 1, lockState.getLockKey(), lockState.getLockValue());
            //解锁成功
            if (UNLOCK_SUCCESS.equals(result)) {
                log.info("[jedis-lock] unlock success 线程 : {} 解锁成功，锁key : {} ,路径：{}", Thread.currentThread().getName(), lockState.getLockKey(), lockState.getLockValue());
            } else {
                log.info("[jedis-lock] unlock del key failed ，线程 : {} 解锁成功，锁key : {} ,路径：{}", Thread.currentThread().getName(), lockState.getLockKey(), lockState.getLockValue());
            }
        } catch (Exception e) {
            log.error("[jedis-lock] unlock failed:" + e.getMessage(), e);
            throw new DtLockException("[jedis-lock] unlock failed:" + e.getMessage(), e);
        }
    }

    /**
     * 定时将 Key 的过期时间推迟
     */
    private void manualKeepAlive() {

        log.debug("开启定时将 Key 的过期时间推迟任务");

        final String t_key = lockState.getLockKey();
        final int t_ttl = lockState.getLeaseTTL();

        keepAliveTask = new KeepAliveTask(() -> {
            // 刷新值
            try {
                log.debug("刷新 Key 的过期时间");
                client.expire(t_key, t_ttl);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, t_ttl);
        keepAliveTask.start();
    }

    /**
     * 锁的封装
     */
    class LockState {
        //锁的键
        private String lockKey;
        //锁的值
        private String lockValue;
        //错误信息
        private String errorMsg;
        //锁的过期时间
        private int leaseTTL;
        private long leaseId;
        //是否加锁成功
        private boolean lockSuccess;

        public LockState(String lockKey, int leaseTTL) {
            this.lockKey = lockKey;
            this.leaseTTL = leaseTTL;
        }

        public LockState(String lockKey, String value, int leaseTTL) {
            this.lockKey = lockKey;
            this.lockValue = value;
            this.leaseTTL = leaseTTL;
        }

        public String getLockKey() {
            return lockKey;
        }

        public void setLockKey(String lockKey) {
            this.lockKey = lockKey;
        }

        public String getLockValue() {
            return lockValue;
        }

        public void setLockValue(String lockValue) {
            this.lockValue = lockValue;
        }

        public String getErrorMsg() {
            return errorMsg;
        }

        public void setErrorMsg(String errorMsg) {
            this.errorMsg = errorMsg;
        }

        public long getLeaseId() {
            return leaseId;
        }

        public void setLeaseId(long leaseId) {
            this.leaseId = leaseId;
        }

        public boolean isLockSuccess() {
            return lockSuccess;
        }

        public void setLockSuccess(boolean lockSuccess) {
            this.lockSuccess = lockSuccess;
        }

        public int getLeaseTTL() {
            return leaseTTL;
        }

        public void setLeaseTTL(int leaseTTL) {
            this.leaseTTL = leaseTTL;
        }
    }
}
